You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

273 lines
7.3 KiB

<script setup lang="ts">
import { request, unwrapApiBody, type ApiResponse } from '../../../utils/http/factory'
import { useAuthSession } from '../../../composables/useAuthSession'
definePageMeta({ title: '编辑文章' })
type PostRow = { id: number; title: string; slug: string; visibility: string }
const route = useRoute()
const id = computed(() => route.params.id as string)
const { user, refresh: refreshAuth } = useAuthSession()
const state = reactive({
title: '',
slug: '',
excerpt: '',
bodyMarkdown: '',
visibility: 'private',
shareToken: '' as string | null,
})
const loading = ref(true)
const saving = ref(false)
const postsNav = ref<PostRow[]>([])
const currentNumericId = computed(() => Number.parseInt(id.value, 10))
const newerPost = computed((): PostRow | null => {
const list = postsNav.value
const idx = list.findIndex((p) => p.id === currentNumericId.value)
if (idx <= 0) {
return null
}
return list[idx - 1] ?? null
})
const olderPost = computed((): PostRow | null => {
const list = postsNav.value
const idx = list.findIndex((p) => p.id === currentNumericId.value)
if (idx < 0 || idx >= list.length - 1) {
return null
}
return list[idx + 1] ?? null
})
const publicPostHref = computed(() => {
const ps = user.value?.publicSlug
if (state.visibility !== 'public' || !ps || !state.slug) {
return ''
}
return `/@${ps}/posts/${encodeURIComponent(state.slug)}`
})
/** 公开文跳站点详情,否则进编辑页 */
function navTargetHref(p: PostRow | null) {
if (!p) {
return ''
}
const ps = user.value?.publicSlug
if (p.visibility === 'public' && ps) {
return `/@${ps}/posts/${encodeURIComponent(p.slug)}`
}
return `/me/posts/${p.id}`
}
async function load() {
loading.value = true
try {
const res = await request<ApiResponse<{ post: typeof state }>>(`/api/me/posts/${id.value}`)
const p = unwrapApiBody(res).post
Object.assign(state, {
title: p.title,
slug: p.slug,
excerpt: p.excerpt,
bodyMarkdown: p.bodyMarkdown,
visibility: p.visibility,
shareToken: p.shareToken ?? null,
})
} finally {
loading.value = false
}
}
async function loadPostNav() {
try {
const res = await request<ApiResponse<{ posts: PostRow[] }>>('/api/me/posts')
postsNav.value = unwrapApiBody(res).posts
} catch {
postsNav.value = []
}
}
onMounted(() => {
void refreshAuth(true)
void loadPostNav()
void load()
})
watch(id, () => {
void load()
})
async function save() {
saving.value = true
try {
await request(`/api/me/posts/${id.value}`, {
method: 'PUT',
body: {
title: state.title,
slug: state.slug,
excerpt: state.excerpt,
bodyMarkdown: state.bodyMarkdown,
visibility: state.visibility,
},
})
await load()
} finally {
saving.value = false
}
}
async function remove() {
await request(`/api/me/posts/${id.value}`, { method: 'DELETE' })
await navigateTo('/me/posts')
}
const shareUrl = computed(() => {
const slug = user.value?.publicSlug
if (state.visibility !== 'unlisted' || !state.shareToken || !slug) {
return ''
}
if (import.meta.client) {
return `${window.location.origin}/p/${slug}/t/${state.shareToken}`
}
return ''
})
</script>
<template>
<UContainer class="py-8 max-w-6xl space-y-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<h1 class="text-2xl font-semibold tracking-tight">
编辑文章
</h1>
<div class="flex flex-wrap items-center gap-2">
<UButton
v-if="publicPostHref"
:to="publicPostHref"
variant="soft"
color="neutral"
size="sm"
>
详情
</UButton>
<UButton to="/me/posts" variant="ghost" color="neutral" size="sm">
返回列表
</UButton>
</div>
</div>
<UCard v-if="!loading" :ui="{ body: 'p-3 sm:p-4' }">
<div class="flex flex-wrap items-center gap-2">
<UButton
v-if="newerPost"
:to="navTargetHref(newerPost)"
variant="soft"
color="neutral"
size="sm"
leading-icon="i-lucide-chevron-up"
>
较新
</UButton>
<UButton
v-else
disabled
variant="soft"
color="neutral"
size="sm"
leading-icon="i-lucide-chevron-up"
>
较新
</UButton>
<UButton
v-if="olderPost"
:to="navTargetHref(olderPost)"
variant="soft"
color="neutral"
size="sm"
leading-icon="i-lucide-chevron-down"
>
较旧
</UButton>
<UButton
v-else
disabled
variant="soft"
color="neutral"
size="sm"
leading-icon="i-lucide-chevron-down"
>
较旧
</UButton>
</div>
<p v-if="newerPost || olderPost" class="text-xs text-muted mt-2">
顺序与列表一致(最新在上)。公开文章打开站点详情,其余进入对应编辑页。
</p>
</UCard>
<div v-if="loading" class="text-muted">
加载中…
</div>
<template v-else>
<UForm :state="state" class="space-y-6" @submit.prevent="save">
<UCard :ui="{ body: 'p-4 sm:p-6' }">
<PostBodyMarkdownEditor v-model="state.bodyMarkdown" />
<UFormField label="正文" name="bodyMarkdown" required class="sr-only" />
</UCard>
<UCard :ui="{ body: 'p-4 sm:p-6 space-y-4' }">
<UCollapsible :unmount-on-hide="false">
<UButton
type="button"
color="neutral"
variant="subtle"
block
class="justify-between font-medium"
label="文章设置"
trailing
trailing-icon="i-lucide-chevron-down"
/>
<template #content>
<div class="pt-4 space-y-4 border-t border-default mt-4">
<UAlert
v-if="shareUrl"
title="仅链接分享"
:description="shareUrl"
/>
<UFormField label="标题" name="title" required>
<UInput v-model="state.title" />
</UFormField>
<UFormField label="slug" name="slug" required>
<UInput v-model="state.slug" />
</UFormField>
<UFormField label="摘要" name="excerpt" required>
<UInput v-model="state.excerpt" />
</UFormField>
<UFormField label="可见性" name="visibility">
<USelect
v-model="state.visibility"
:items="[
{ label: '私密', value: 'private' },
{ label: '公开', value: 'public' },
{ label: '仅链接', value: 'unlisted' },
]"
/>
</UFormField>
</div>
</template>
</UCollapsible>
</UCard>
<div class="flex flex-wrap gap-2">
<UButton type="submit" :loading="saving">
保存
</UButton>
<UButton color="error" variant="soft" type="button" @click="remove">
删除
</UButton>
</div>
</UForm>
</template>
</UContainer>
</template>